// Copyright DustedPixels.com 2008. All rights reserved.

package com.dustedpixels.jasmin.cpu;

import java.util.Arrays;

import com.dustedpixels.common.Threads;
import com.dustedpixels.jasmin.common.SharedMemory;

/**
 * @author micapolos@gmail.com (Michal Pociecha-Los)
 */
public class SharedMemoryKit {
  private final int count;
  
  private final BufferItem[][] buffers;
  private final int bufferSizeMask;
  
  private final byte[][] memories;
  private final int addressMask;
  
  private final long waitTimes[];
  
  private final Object globalMutex = new Object(){};
  
  private final BufferItem[] globalBuffer;
  private final BufferItem[] tempBuffer;
  private final int globalBufferSizeMask;
  private int globalBufferPeekIndex;
  private int globalBufferPokeIndex;
  
  private final SharedMemoryImpl[] sharedMemoryImpls;
  
  private int synced;
  
  private final int[] globalBufferIndices;
  private long globalNoPokeUntil;
  private long globalNoPeekUntil;
    
  public SharedMemoryKit(int count, int addressBits, int bufferSizeBits) {
    this.buffers = new BufferItem[1 << bufferSizeBits][count];
    this.bufferSizeMask = (1 << bufferSizeBits) - 1;
    
    this.memories = new byte[1 << addressBits][count];
    this.addressMask = (1 << addressBits) - 1;
    
    this.waitTimes = new long[count];
    for (int i = 0; i < count; i++) {
      this.waitTimes[i] = Long.MIN_VALUE;
    }

    this.globalBufferIndices = new int[count];
    this.globalNoPokeUntil = 0;
    this.globalNoPeekUntil = 0;
  }
  
  private void sync() {
    synchronized (globalMutex) {
      // Wait until all threads are synced
      synced++;
    
      if (synced != count) {
        Threads.waitOn(globalMutex);        
      }
      
      // Determine minimum sync time, nearest and second nearest possible poke times.
      long syncTime = Long.MAX_VALUE;
      long nearestPokeTime = Long.MAX_VALUE;
      long secondNearestPokeTime = Long.MAX_VALUE;
      for (int i = 0; i < count; i++) {
        SharedMemoryImpl memory = sharedMemoryImpls[i];
        syncTime = Math.min(syncTime, memory.syncTime);
        if (memory.noPokeUntil < nearestPokeTime) {
          secondNearestPokeTime = nearestPokeTime;
          nearestPokeTime = memory.noPokeUntil;
        }
      }
      
      for (int i = 0; i < count; i++) {
        SharedMemoryImpl memory = sharedMemoryImpls[i];
        memory.nearestPokeTime = (memory.noPokeUntil != nearestPokeTime)
            ? nearestPokeTime
            : secondNearestPokeTime;
      }
      
      // Copy local buffers to global buffer.
      int tempBufferIndex = 0;
      for (int i = 0; i < count; i++) {
        SharedMemoryImpl memory = sharedMemoryImpls[i];
        boolean done = false;
        do {
          if (memory.bufferStart == memory.bufferIndex && !memory.bufferFull) {
            done = true;
          } else {
            tempBuffer[tempBufferIndex] = memory.buffer[memory.bufferStart];
            memory.bufferStart = (memory.bufferStart + 1) & bufferSizeMask;
            memory.bufferFull = false;
            tempBufferIndex++;
          }
        } while (!done);
      }
      
      // Sort temp buffer by time.
      Arrays.sort(tempBuffer, 0, tempBufferIndex);
      
      for (int i = 0; i < tempBufferIndex; i++) {
        globalBuffer[globalBufferPokeIndex] = tempBuffer[i];
        globalBufferPokeIndex = (globalBufferPokeIndex + 1) & globalBufferSizeMask;
      }
    }
  }
  
  private final class SharedMemoryImpl implements SharedMemory {
    private final int index;
    
    private final BufferItem[] buffer;
    private int bufferIndex;
    private int bufferStart;
    private boolean bufferFull;
    
    private final byte[] memory;
    
    private int globalBufferIndex;
    private int globalBufferLimit;
    private boolean globalBufferFull;
    
    private long noPokeUntil;
    private long noPeekUntil;
    
    private long syncTime;
    private long nearestPokeTime;
    
    private final Object mutex = new Object(){};
    
    public SharedMemoryImpl(int index) {
      this.index = index;
      this.buffer = new BufferItem[bufferSizeMask];
      this.memory = memories[index];
    }
    
    public byte peek(long time, int address) {
      while (time > nearestPokeTime) {
        syncTime = time;
        sync();
      }
      
      boolean done = false;
      
      do {
        if (globalBufferIndex == globalBufferLimit && !globalBufferFull) {
          done = true;
        } else {
          BufferItem bufferItem = globalBuffer[globalBufferIndex];

          if (bufferItem.time > time) {
            done = true;
          } else {
            memory[bufferItem.address] = bufferItem.value;
            globalBufferIndex = (globalBufferIndex + 1) & globalBufferSizeMask;
            globalBufferFull = false;
          }
        }
      } while (!done);

      return memory[address & addressMask];
    }

    public void poke(long time, int address, byte value) {
      // Add poked value to buffer.
      BufferItem bufferItem = buffer[bufferIndex];
      bufferItem.index = index;
      bufferItem.time = time;
      bufferItem.address = address;
      bufferItem.value = value;
      bufferIndex = (bufferIndex + 1) & bufferSizeMask;
      
      if (bufferIndex == bufferStart) {
        bufferFull = true;
      }
      
      while (bufferFull) {
        syncTime = time;
        sync();
      }
    }
    
    public void noPeekUntil(long time) {
      noPeekUntil = time;
    }

    public void noPokeUntil(long time) {
      noPokeUntil = time;
    }
  }
  
  private final class BufferItem implements Comparable<BufferItem> {
    int index;
    long time;
    int address;
    byte value;
    
    public int compareTo(BufferItem other) {
      return (time == other.time) ? 0 : (time > other.time) ? 1 : -1; 
    }
  }
}
